package org.chipu.shared.util.file;

import java.io.RandomAccessFile;
import java.util.*;

public class FileHelper {
	public static List<String> tail(String fileName, int lineCount) {
		return tail(fileName, lineCount, 2000);
	}

	/**
	 * Reads last N lines from the given file. File reading is done in chunks. Constraints: 1 Minimize the number of
	 * file reads -- Avoid reading the complete file to get last few lines. 2 Minimize the JVM in-memory usage -- Avoid
	 * storing the complete file info in in-memory. Approach: Read a chunk of characters from end of file. One chunk
	 * should contain multiple lines. Reverse this chunk and extract the lines. Repeat this until you get required
	 * number of last N lines. In this way we read and store only the required part of the file. 1 Create a
	 * RandomAccessFile. 2 Get the position of last character using (i.e length-1). Let this be curPos. 3 Move the
	 * cursor to fromPos = (curPos - chunkSize). Use seek(). 4 If fromPos is less than or equal to ZERO then go to
	 * step-5. Else go to step-6 5 Read characters from beginning of file to curPos. Go to step-9. 6 Read 'chunksize'
	 * characters from fromPos. 7 Extract the lines. On reading required N lines go to step-9. 8 Repeat step 3 to 7
	 * until a. N lines are read. OR b. All lines are read when num of lines in file is less than N. Last line may be a
	 * incomplete, so discard it. Modify curPos appropriately. 9 Exit. Got N lines or less than that.
	 */
	public static List<String> tail(String fileName, int lineCount, int chunkSize) {
		try {
			RandomAccessFile raf = new RandomAccessFile(fileName, "r");
			List<String> lastNlines = new ArrayList<String>();
			int delta = 0;
			long curPos = raf.length() - 1;
			long fromPos;
			byte[] bytearray;
			while(true) {
				fromPos = curPos - chunkSize;
				System.out.println(curPos);
				System.out.println(fromPos);
				if (fromPos <= 0) {
					raf.seek(0);
					bytearray = new byte[(int) curPos];
					raf.readFully(bytearray);
					parseLinesFromLast(bytearray, lineCount, lastNlines);
					break;
				}
				raf.seek(fromPos);
				bytearray = new byte[chunkSize];
				raf.readFully(bytearray);
				if (parseLinesFromLast(bytearray, lineCount, lastNlines))
					break;
				delta = (lastNlines.get(lastNlines.size() - 1)).length();
				lastNlines.remove(lastNlines.size() - 1);
				curPos = fromPos + delta;
			}
			return lastNlines;
		} catch(Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * Given a byte array this method: a. creates a String out of it b. reverses the string c. extracts the lines d.
	 * characters in extracted line will be in reverse order, so it reverses the line just before storing in the list.
	 * On extracting required number of lines, this method returns TRUE, Else it returns FALSE.
	 * @return A boolean indicating if it managed to read 'lineCount' lines
	 */
	private static boolean parseLinesFromLast(byte[] bytearray, int lineCount, List<String> lastNlines) {
		String lastNChars = new String(bytearray);
		StringBuffer sb = new StringBuffer(lastNChars);
		lastNChars = sb.reverse().toString();
		StringTokenizer tokens = new StringTokenizer(lastNChars, "\n");
		while(tokens.hasMoreTokens()) {
			StringBuffer sbLine = new StringBuffer(tokens.nextToken());
			lastNlines.add(sbLine.reverse().toString());
			if (lastNlines.size() == lineCount)
				return true;
		}
		return false;
	}
}